En djupdykning i JavaScripts asynkrona iteratorhjÀlpare 'scan', som utforskar dess funktionalitet, anvÀndningsfall och fördelar för asynkron ackumulativ bearbetning.
JavaScript Asynkron IteratorhjÀlpare: Scan - Asynkron Ackumulativ Bearbetning
Asynkron programmering Àr en hörnsten i modern JavaScript-utveckling, sÀrskilt nÀr man hanterar I/O-bundna operationer, som nÀtverksanrop eller filsystemsinteraktioner. Asynkrona iteratorer, introducerade i ES2018, tillhandahÄller en kraftfull mekanism för att hantera strömmar av asynkron data. `scan`-hjÀlparen, som ofta finns i bibliotek som RxJS och blir alltmer tillgÀnglig som ett fristÄende verktyg, lÄser upp Ànnu mer potential för att bearbeta dessa asynkrona dataströmmar.
FörstÄelse för Asynkrona Iteratorer
Innan vi dyker in i `scan`, lÄt oss sammanfatta vad asynkrona iteratorer Àr. En asynkron iterator Àr ett objekt som följer det asynkrona iteratorprotokollet. Detta protokoll definierar en `next()`-metod som returnerar ett löfte (promise) som löser sig till ett objekt med tvÄ egenskaper: `value` (nÀsta vÀrde i sekvensen) och `done` (en boolean som indikerar om iteratorn Àr klar). Asynkrona iteratorer Àr sÀrskilt anvÀndbara nÀr man arbetar med data som anlÀnder över tid, eller data som krÀver asynkrona operationer för att hÀmtas.
HÀr Àr ett grundlÀggande exempel pÄ en asynkron iterator:
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
async function main() {
const iterator = generateNumbers();
let result = await iterator.next();
console.log(result); // { value: 1, done: false }
result = await iterator.next();
console.log(result); // { value: 2, done: false }
result = await iterator.next();
console.log(result); // { value: 3, done: false }
result = await iterator.next();
console.log(result); // { value: undefined, done: true }
}
main();
Introduktion till `scan`-hjÀlparen
`scan`-hjÀlparen (Àven kÀnd som `accumulate` eller `reduce`) transformerar en asynkron iterator genom att tillÀmpa en ackumulatorfunktion pÄ varje vÀrde och emittera det ackumulerade resultatet. Detta Àr analogt med `reduce`-metoden pÄ arrayer, men fungerar asynkront och pÄ iteratorer.
I grund och botten tar `scan` en asynkron iterator, en ackumulatorfunktion och ett valfritt initialt vÀrde. För varje vÀrde som emitteras av kÀlliteratorn anropas ackumulatorfunktionen med det föregÄende ackumulerade vÀrdet (eller det initiala vÀrdet om det Àr den första iterationen) och det nuvarande vÀrdet frÄn iteratorn. Resultatet av ackumulatorfunktionen blir det nya ackumulerade vÀrdet, som sedan emitteras av den resulterande asynkrona iteratorn.
Syntax och Parametrar
Den allmÀnna syntaxen för att anvÀnda `scan` Àr som följer:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator`: Den asynkrona iteratorn som ska transformeras.
- `accumulator`: En funktion som tar tvÄ argument: det föregÄende ackumulerade vÀrdet och det nuvarande vÀrdet frÄn iteratorn. Den bör returnera det nya ackumulerade vÀrdet.
- `initialValue` (valfri): Det initiala vÀrdet för ackumulatorn. Om det inte anges kommer det första vÀrdet frÄn kÀlliteratorn att anvÀndas som initialt vÀrde, och ackumulatorfunktionen kommer att anropas frÄn och med det andra vÀrdet.
AnvÀndningsfall och Exempel
`scan`-hjÀlparen Àr otroligt mÄngsidig och kan anvÀndas i en mÀngd olika scenarier som involverar asynkrona dataströmmar. HÀr Àr nÄgra exempel:
1. BerÀkna en löpande summa
FörestÀll dig att du har en asynkron iterator som emitterar transaktionsbelopp. Du kan anvÀnda `scan` för att berÀkna en löpande summa av dessa transaktioner.
async function* generateTransactions() {
yield 10;
yield 20;
yield 30;
}
async function main() {
const transactions = generateTransactions();
const runningTotals = scan(transactions, (acc, value) => acc + value, 0);
for await (const total of runningTotals) {
console.log(total); // Output: 10, 30, 60
}
}
main();
I detta exempel lÀgger `accumulator`-funktionen helt enkelt till det aktuella transaktionsbeloppet till den föregÄende summan. `initialValue` pÄ 0 sÀkerstÀller att den löpande summan börjar pÄ noll.
2. Ackumulera data i en array
Du kan anvÀnda `scan` för att ackumulera data frÄn en asynkron iterator i en array. Detta kan vara anvÀndbart för att samla in data över tid och bearbeta den i batcher.
async function* fetchData() {
yield { id: 1, name: 'Alice' };
yield { id: 2, name: 'Bob' };
yield { id: 3, name: 'Charlie' };
}
async function main() {
const dataStream = fetchData();
const accumulatedData = scan(dataStream, (acc, value) => [...acc, value], []);
for await (const data of accumulatedData) {
console.log(data); // Output: [{id: 1, name: 'Alice'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
}
}
main();
HÀr anvÀnder `accumulator`-funktionen spread-operatorn (`...`) för att skapa en ny array som innehÄller alla föregÄende element och det nuvarande vÀrdet. `initialValue` Àr en tom array.
3. Implementera en Rate Limiter (hastighetsbegrÀnsare)
Ett mer komplext anvÀndningsfall Àr att implementera en rate limiter (hastighetsbegrÀnsare). Du kan anvÀnda `scan` för att spÄra antalet anrop som görs inom ett visst tidsfönster och fördröja efterföljande anrop om hastighetsgrÀnsen överskrids.
async function* generateRequests() {
// Simulate incoming requests
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 200));
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
yield Date.now();
}
async function main() {
const requests = generateRequests();
const rateLimitWindow = 1000; // 1 second
const maxRequestsPerWindow = 2;
async function* rateLimitedRequests(source, window, maxRequests) {
let queue = [];
for await (const requestTime of source) {
queue.push(requestTime);
queue = queue.filter(t => requestTime - t < window);
if (queue.length > maxRequests) {
const earliestRequest = queue[0];
const delay = window - (requestTime - earliestRequest);
console.log(`Rate limit exceeded. Delaying for ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
yield requestTime;
}
}
const limited = rateLimitedRequests(requests, rateLimitWindow, maxRequestsPerWindow);
for await (const requestTime of limited) {
console.log(`Request processed at ${requestTime}`);
}
}
main();
Detta exempel anvÀnder `scan` internt (i `rateLimitedRequests`-funktionen) för att upprÀtthÄlla en kö av tidsstÀmplar för anrop. Den kontrollerar om antalet anrop inom hastighetsgrÀnsfönstret överskrider det maximalt tillÄtna. Om sÄ Àr fallet berÀknar den nödvÀndig fördröjning och pausar innan den yieldar anropet.
4. Bygga en datainsamlare i realtid (Globalt exempel)
TÀnk dig en global finansiell applikation som behöver aggregera aktiekurser i realtid frÄn olika börser. En asynkron iterator skulle kunna strömma kursuppdateringar frÄn börser som New York Stock Exchange (NYSE), London Stock Exchange (LSE) och Tokyo Stock Exchange (TSE). `scan` kan anvÀndas för att upprÀtthÄlla ett löpande medelvÀrde eller högsta/lÀgsta pris för en viss aktie över alla börser.
// Simulate streaming stock prices from different exchanges
async function* generateStockPrices() {
yield { exchange: 'NYSE', symbol: 'AAPL', price: 170.50 };
yield { exchange: 'LSE', symbol: 'AAPL', price: 170.75 };
await new Promise(resolve => setTimeout(resolve, 50));
yield { exchange: 'TSE', symbol: 'AAPL', price: 170.60 };
}
async function main() {
const stockPrices = generateStockPrices();
// Use scan to calculate a running average price
const runningAverages = scan(
stockPrices,
(acc, priceUpdate) => {
const { total, count } = acc;
return { total: total + priceUpdate.price, count: count + 1 };
},
{ total: 0, count: 0 }
);
for await (const averageData of runningAverages) {
const averagePrice = averageData.total / averageData.count;
console.log(`Running average price: ${averagePrice.toFixed(2)}`);
}
}
main();
I detta exempel berÀknar `accumulator`-funktionen den löpande summan av kurserna och antalet mottagna uppdateringar. Det slutliga medelpriset berÀknas sedan frÄn dessa ackumulerade vÀrden. Detta ger en realtidsvy av aktiekursen över olika globala marknader.
5. Analysera webbplatstrafik globalt
FörestÀll dig en global webbanalysplattform som tar emot strömmar av data om webbplatsbesök frÄn servrar runt om i vÀrlden. Varje datapunkt representerar en anvÀndare som besöker webbplatsen. Med `scan` kan vi analysera trenden för sidvisningar per land i realtid. LÄt oss sÀga att datan ser ut sÄ hÀr: `{ country: "US", page: "homepage", timestamp: 1678886400 }`.
async function* generateWebsiteVisits() {
yield { country: 'US', page: 'homepage', timestamp: Date.now() };
yield { country: 'CA', page: 'product', timestamp: Date.now() };
yield { country: 'UK', page: 'blog', timestamp: Date.now() };
yield { country: 'US', page: 'product', timestamp: Date.now() };
}
async function main() {
const visitStream = generateWebsiteVisits();
const pageViewCounts = scan(
visitStream,
(acc, visit) => {
const { country } = visit;
const newAcc = { ...acc };
newAcc[country] = (newAcc[country] || 0) + 1;
return newAcc;
},
{}
);
for await (const counts of pageViewCounts) {
console.log('Page view counts by country:', counts);
}
}
main();
HÀr uppdaterar `accumulator`-funktionen en rÀknare för varje land. Utdata skulle visa de ackumulerande sidvisningsrÀkningarna för varje land nÀr ny besöksdata anlÀnder.
Fördelar med att anvÀnda `scan`
`scan`-hjÀlparen erbjuder flera fördelar nÀr man arbetar med asynkrona dataströmmar:
- Deklarativ stil: `scan` lÄter dig uttrycka ackumulativ bearbetningslogik pÄ ett deklarativt och koncist sÀtt, vilket förbÀttrar kodens lÀsbarhet och underhÄllbarhet.
- Asynkron hantering: Den hanterar sömlöst asynkrona operationer inom ackumulatorfunktionen, vilket gör den lÀmplig för komplexa scenarier som involverar I/O-bundna uppgifter.
- Realtidsbearbetning: `scan` möjliggör realtidsbearbetning av dataströmmar, vilket lÄter dig reagera pÄ förÀndringar nÀr de intrÀffar.
- Komponerbarhet: Den kan enkelt komponeras med andra asynkrona iteratorhjÀlpare för att skapa komplexa databearbetningskedjor.
Implementera `scan` (om den inte Àr tillgÀnglig)
Medan vissa bibliotek tillhandahÄller en inbyggd `scan`-hjÀlpare, kan du enkelt implementera din egen om det behövs. HÀr Àr en enkel implementering:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
let first = true;
for await (const value of sourceIterator) {
if (first && initialValue === undefined) {
accumulatedValue = value;
first = false;
} else {
accumulatedValue = accumulator(accumulatedValue, value);
}
yield accumulatedValue;
}
}
Denna implementering itererar över kÀlliteratorn och tillÀmpar ackumulatorfunktionen pÄ varje vÀrde, och yieldar det ackumulerade resultatet. Den hanterar fallet dÀr inget `initialValue` anges genom att anvÀnda det första vÀrdet frÄn kÀlliteratorn som initialt vÀrde.
JÀmförelse med `reduce`
Det Àr viktigt att skilja `scan` frÄn `reduce`. Medan bÄda opererar pÄ iteratorer och anvÀnder en ackumulatorfunktion, skiljer de sig i sitt beteende och utdata.
- `scan` emitterar det ackumulerade vÀrdet för varje iteration, vilket ger en löpande historik över ackumuleringen.
- `reduce` emitterar endast det slutliga ackumulerade vÀrdet efter att ha bearbetat alla element i iteratorn.
DÀrför Àr `scan` lÀmplig för scenarier dÀr du behöver spÄra de mellanliggande tillstÄnden av ackumuleringen, medan `reduce` Àr lÀmplig nÀr du bara behöver det slutliga resultatet.
Felhantering
NÀr man arbetar med asynkrona iteratorer och `scan` Àr det avgörande att hantera fel pÄ ett elegant sÀtt. Fel kan uppstÄ under iterationsprocessen eller inom ackumulatorfunktionen. Du kan anvÀnda `try...catch`-block för att fÄnga och hantera dessa fel.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Something went wrong!');
yield 3;
}
async function main() {
const dataStream = generatePotentiallyFailingData();
try {
const accumulatedData = scan(dataStream, (acc, value) => acc + value, 0);
for await (const data of accumulatedData) {
console.log(data);
}
} catch (error) {
console.error('An error occurred:', error);
}
}
main();
I detta exempel fÄngar `try...catch`-blocket felet som kastas av `generatePotentiallyFailingData`-iteratorn. Du kan sedan hantera felet pÄ lÀmpligt sÀtt, till exempel genom att logga det eller försöka operationen igen.
Sammanfattning
`scan`-hjÀlparen Àr ett kraftfullt verktyg för att utföra asynkron ackumulativ bearbetning pÄ JavaScripts asynkrona iteratorer. Den lÄter dig uttrycka komplexa datatransformationer pÄ ett deklarativt och koncist sÀtt, hantera asynkrona operationer elegant och bearbeta dataströmmar i realtid. Genom att förstÄ dess funktionalitet och anvÀndningsfall kan du utnyttja `scan` för att bygga mer robusta och effektiva asynkrona applikationer. Oavsett om du berÀknar löpande summor, ackumulerar data i arrayer, implementerar rate limiters eller bygger datainsamlare i realtid, kan `scan` förenkla din kod och förbÀttra dess övergripande prestanda. Kom ihÄg att övervÀga felhantering och att vÀlja `scan` över `reduce` nÀr du behöver tillgÄng till mellanliggande ackumulerade vÀrden under bearbetningen av dina asynkrona dataströmmar. Att utforska bibliotek som RxJS kan ytterligare förbÀttra din förstÄelse och praktiska tillÀmpning av `scan` inom reaktiva programmeringsparadigm.